04. Test Repository with Constructor Dependency Injection and DI

L5 P2 A10 Test The Repository Using Constructor Dependency Injection And DI V2

In this step you'll implement Constructor Dependency Injection. Constructor dependency injection allows you to swap in the test double by passing it into the constructor.

Step 1: Use Constructor Dependency Injection in DefaultTasksRepository

  1. Change the DefaultTaskRepository's constructor from taking in an Application to taking in both data sources and the coroutine dispatcher :

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }


// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Because we passed the dependencies in, remove the init method. You no longer need to create the dependencies.
  2. Also delete the old instance variables. You're defining them in the constructor:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Finally, update the getRepository method to use the new constructor:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

You are now using constructor dependency injection!

Step 2: Use your FakeDataSource in your tests

Now that your code is using constructor dependency injection, you can use your fake data source to test your DefaultTasksRepository.

  1. Right click on the DefaultTasksRepository class name and select Generate then Test.
  2. Follow the prompts to create DefaultTasksRepositoryTest in the test source set.
  3. At the top of your new DefaultTasksRepositoryTest class, add these member variables to represent the data in your fake data sources:

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Also create three variables - two FakeDataSource member variables (one for each data source for your repository) and a variable for the DefaultTasksRepository which you will test:

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Now you'll make a method to setup and initialize a testable DefaultTasksRepository. This DefaultTasksRepository will use your test double FakeDataSources.

  1. Create a method called createRepository and annotate it with @Before.
  2. Instantiate your fake data sources, using the remoteTasks and localTasks lists.
  3. Instantiate your tasksRepository, using the two fake data sources you just created and Dispatchers.Unconfined.

The final method should look like this:

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

Step 3: Write DefaultTasksRepository's getTasks() Test

Time to write a DefaultTasksRepository test!

  1. Write a test for the repository's getTasks method. Check that when you call get tasks with true (meaning that it should reload from the remote data source) that it returns data from the remote data source (as opposed to the local data source):

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

You will get an error when you call getTasks:

Step 4: Add runBlockingTest

  1. Add the required dependencies for testing coroutines to the test source set by using testImplementation:

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Don't forget to sync!

You should use runBlockingTest in your test classes when you're calling a suspend function.

  1. Add the @ExperimentalCoroutinesApi above the class.
  2. Back in your DefaultTasksRepositoryTest add runBlockingTest so that it takes in your entire test as a "block" of code

This final test looks like:

DefaultTasksRepositoryTest.kt

@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. Run your new getTasks_requestsAllTasksFromRemoteDataSource test and confirm it works and the error is gone! Splendid!